package Algorithms;

import java.util.ArrayList;
import java.util.List;
import piis.TestData;

public class LinearRegression {
	
	double[][] x;
	double[] y;
	double[] params;
	double[] errors;
	double alpha = 0.05;
	int iterations = 50000;
        private List<String> trainInput = new ArrayList<>();
        private List<String> trainInput3 = new ArrayList<>();

        public LinearRegression(int params){
		this.params = new double[params];
	}
	
	public LinearRegression(double[][] x, double[] y, int params){
		this.x=x;
		this.y=y;
		this.params = new double[params];
	}
        
        //x is basically row=different x for a given y with corresponding index in y
        public LinearRegression(List<List<Double>> x, double[] y, int params, int dim){
		this.y=y;
//		this.params = new double[params];
                this.x = prepareData(x,y,params,dim);
                trainInput = new ArrayList<String>();
	}
        
        private double[][] prepareData(List<List<Double>> x, double[] y, int degree, int dim){
            
            List<List<Double>> tempArray = new ArrayList<>(x.size());
            List<Integer> temp;
            
            System.out.println("x sizes: "+x.size()+" "+x.get(0).size()+"  y: "+y.length+" N: "+degree+" V: "+dim);
            // for each groups of x values, so for each row
            for(int i = 0; i < x.size(); i++){
                tempArray.add(new ArrayList<Double>());
                temp = new ArrayList<>();
                // take each x for the same y.
                for(int d = 0; d < x.get(i).size(); d++){
                    tempArray.get(i).add(x.get(i).get(d));
                    //if(i == 0){System.out.print(x.get(i).get(d)+" ");}
                    temp.add(d);
                }
                for(int j = 2; j <= degree; j++){
                    int lastIndex = tempArray.get(i).size();
                    int prevLastIndex = lastIndex;
                    for(int p = 0; p < dim; p++){
                        for(int r = temp.get(p); r < lastIndex; r++){
                            tempArray.get(i).add(x.get(i).get(p) * tempArray.get(i).get(r));
                            //if(i == 0){System.out.print("("+x.get(i).get(p)+" * "+tempArray.get(i).get(r)+") ");}
                        }
                        temp.set(p, prevLastIndex);
                        prevLastIndex = tempArray.get(i).size();
                    }
                }
            }
            if(!tempArray.isEmpty()){
                double[][] results = new double[tempArray.get(0).size()][tempArray.size()];
                for(int i = 0; i < tempArray.size(); i++){
                    for(int j = 0; j < tempArray.get(i).size(); j++){
                        results[j][i] = tempArray.get(i).get(j);
                    }
                }
                System.out.println("RESULTS: "+results.length+" "+ results[0].length);
                this.params = new double[results.length];
                return results;
            } else {
                this.params = new double[degree];
                return null;
            }
        }
	
	public void setParams(double[] params) {
		this.params = params;
	}
	
	public double[] getErrors(){
		return errors;
	}
	
	public double[] getParams(){
		return params;
	}
	
	/**
	 * This function call to the gradient descent algorithm, in order to 
	 * reduce the error doing i iterations of the method
	 * It call to setParams and getError.
	 */
	
	public void gradientDescent(){
		errors = new double[iterations];
		for(int i=0; i < iterations; i++){
			params = setParams();
			errors[i] = getError();
		}
	}
        
	public void gradientDescentSI(){
		errors = new double[iterations];
                String input;
		for(int i=0; i < iterations; i++){
			params = setParams();
			errors[i] = getError();
                        if(i % 500 == 0){
                            input = Double.toString(errors[i])+" ";
                            for(int j = 0; j < params.length; j++){
                                input += Double.toString(params[j])+ " ";
                            }
                            trainInput.add(input);
                        }
		}
//		System.out.println(params);
	}
        
        public void simulatedAnnealing(int iter, double min, double max){
            errors = new double[iter];
            SimulatedAnnealing sa = new SimulatedAnnealing();
                
            for(int i = 0; i < iter; i++){  
                params = sa.findLRSolution(i, params, x, y,min,max);
                errors[i] = getError();
                if(i % 600 == 0){
                    String input = Double.toString(sa.getStartingTemp())+" "+Double.toString(errors[i])+" ";
                    for(int j = 0; j < params.length; j++){
                        input += Double.toString(params[j])+ " ";
                    }
                    trainInput3.add(input);
                }
            }
	}
        
        public void simulatedAnnealing(int iter, double min, double max, List<TestData> data){
            errors = new double[iter];
            SimulatedAnnealing sa = new SimulatedAnnealing("task",max-min);
                
            for(int i = 0; i < iter; i++){  
                params = sa.findSolution(i, params, data, min,max);
                errors[i] = sa.getError(params, data);
                if(i % 600 == 0){
                    String input = Double.toString(sa.getStartingTemp())+" "+Double.toString(errors[i])+" ";
                    for(int j = 0; j < params.length; j++){
                        input += Double.toString(params[j])+ " ";
                    }
                    trainInput3.add(input);
                }
            }
	}
	
	public double getFunction(int i){
		double f = params[0];
		for(int j = 1; j<params.length;j++){
			f+= params[j]*x[j-1][i];
		}
		return f;
	}
	
	/**
	 * This method return the error of the optimized function on the moment of call 
	 * It is only the summatory of the error for each value of the trained set
	 * @return
	 */
	
	public double getError(){
		double error = 0;
		for(int i = 0; i<y.length; i++){
			error += Math.pow((getFunction(i) - y[i]),2);
		}
		error = error / (2*y.length);
		return error;
	}
	
	/**
	 * Calculated the values of the param for the given iterations
	 * First calculated the value of p0 and then from p1..pn
	 * @return
	 */
	public double[] setParams(){
		double[] result = new double[params.length];
		double param = 0;
		//Case p0
		for(int j = 0; j < y.length ;j++){
			param += getFunction(j) - y[j];
		}
		result[0] = params[0] - (alpha * ( param / y.length));
		//Case p1...pn
		for(int i = 1; i < result.length; i++){
			param = 0;
			for(int j = 0; j<y.length;j++){
				param += (getFunction(j) - y[j])*x[i-1][j];
			}
			result[i] = params[i] - (alpha * ( param / y.length));
		}
		
		return result;
		
	}
	
	public double func(double x){
		double result = params[0];
		for(int j = 1; j < params.length ; j++){
			result +=  params[j]*Math.pow(x, j);
		}
		return result;
	}
        
        public double func(double[] x, int degree){
            List<Double> tempArray = new ArrayList<>();
            List<Integer> temp;
            
            double result = 0;
            // for each groups of x values, so for each row
                temp = new ArrayList<>();
                // take each x for the same y.
                for(int d = 0; d < x.length; d++){
                    tempArray.add(x[d]);
                    temp.add(d);
                }
                for(int j = 2; j <= degree; j++){
                    int lastIndex = tempArray.size();
                    int prevLastIndex = lastIndex;
                    for(int p = 0; p < x.length; p++){
                        for(int r = temp.get(p); r < lastIndex; r++){
                            tempArray.add(x[p] * tempArray.get(r));
                        }
                        temp.set(p, prevLastIndex);
                        prevLastIndex = tempArray.size();
                    }
                }
            if(!tempArray.isEmpty()){
                for(int i = 0; i < tempArray.size(); i++){
                    result += (tempArray.get(i) * params[i]);
                }
            }
            return result;
	}

    public double[][] getX() {
        return x;
    }

    public List<String> getTrainInput() {
        return trainInput;
    }

    public List<String> getTrainInput3() {
        return trainInput3;
    }
	
}
